route.ts 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { NextRequest, NextResponse } from "next/server";
  2. import { db } from "@/lib/db";
  3. import { readFile, stat } from "fs/promises";
  4. import path from "path";
  5. const MIME_TYPES: Record<string, string> = {
  6. jpg: "image/jpeg",
  7. jpeg: "image/jpeg",
  8. png: "image/png",
  9. webp: "image/webp",
  10. avif: "image/avif",
  11. svg: "image/svg+xml",
  12. pdf: "application/pdf",
  13. csv: "text/csv",
  14. txt: "text/plain",
  15. mp4: "video/mp4",
  16. webm: "video/webm",
  17. mov: "video/quicktime",
  18. };
  19. export async function GET(
  20. _request: NextRequest,
  21. { params }: { params: Promise<{ token: string; path: string[] }> }
  22. ) {
  23. const { token, path: segments } = await params;
  24. // Expected: [category, filename]
  25. if (segments.length !== 2) {
  26. return NextResponse.json({ error: "Invalid path" }, { status: 400 });
  27. }
  28. const [category, filename] = segments;
  29. const allowedCategories = ["vehicles", "inventory", "services", "logos"];
  30. if (!allowedCategories.includes(category)) {
  31. return NextResponse.json({ error: "Invalid category" }, { status: 400 });
  32. }
  33. // Prevent directory traversal
  34. if (filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
  35. return NextResponse.json({ error: "Invalid filename" }, { status: 400 });
  36. }
  37. // Validate token and get org (check both invoice and quote tokens)
  38. let orgId: string | undefined;
  39. const serviceRecord = await db.serviceRecord.findUnique({
  40. where: { publicToken: token },
  41. select: { vehicle: { select: { organizationId: true } } },
  42. });
  43. if (serviceRecord?.vehicle.organizationId) {
  44. orgId = serviceRecord.vehicle.organizationId;
  45. } else {
  46. const quote = await db.quote.findFirst({
  47. where: { publicToken: token },
  48. select: { organizationId: true },
  49. });
  50. if (quote?.organizationId) {
  51. orgId = quote.organizationId;
  52. } else {
  53. const inspection = await db.inspection.findFirst({
  54. where: { publicToken: token },
  55. select: { organizationId: true },
  56. });
  57. if (inspection?.organizationId) {
  58. orgId = inspection.organizationId;
  59. }
  60. }
  61. }
  62. if (!orgId) {
  63. return NextResponse.json({ error: "Not found" }, { status: 404 });
  64. }
  65. const filePath = path.join(process.cwd(), "data", "uploads", orgId, category, filename);
  66. try {
  67. await stat(filePath);
  68. } catch {
  69. return NextResponse.json({ error: "Not found" }, { status: 404 });
  70. }
  71. const buffer = await readFile(filePath);
  72. const ext = filename.split(".").pop()?.toLowerCase() || "";
  73. const contentType = MIME_TYPES[ext] || "application/octet-stream";
  74. return new NextResponse(buffer, {
  75. headers: {
  76. "Content-Type": contentType,
  77. "Cache-Control": "public, max-age=31536000, immutable",
  78. "X-Robots-Tag": "noindex, nofollow",
  79. },
  80. });
  81. }